Skip to main content

OpenAI Protocol 02, AgentForge OpenAI Integration Core Practices

· 7 min read

Updated: 2026-10-05
Applicable module: agentforge-model-openai
Current implementation: OpenAiChatModel (Blocking), OpenAiStreamingChatModel (SSE)
Wire API: OpenAI Chat Completions
Maintainer: changlu

One-sentence conclusion: The OpenAI Provider does a bidirectional mapping between AgentForge's unified ChatRequest / ChatResponse and the Chat Completions wire format; for the protocol itself see "OpenAI Protocol 01, A Quick Understanding of the OpenAI Low-Level Protocol", and this article covers AgentForge's implementation and boundaries.


1. Background and Problem Introduction​

1.1 Scenario-Driven: One Core, Adapting to Multiple Models​

AgentForge's Agent Runtime depends only on the ChatModel / StreamingChatModel interfaces. We want "switching models" to mean switching just one Provider dependency, rather than changing Agent code.

So the OpenAI Provider's responsibility is limited to being a protocol translation layer:

Agent Runtime
│ ChatRequest / ChatResponse (Provider-agnostic)
▼
OpenAiChatModel / OpenAiStreamingChatModel
│ Chat Completions wire
▼
OpenAI / OpenAI-compatible service

1.2 Problem Guidance: How Do Unified Types Land on the OpenAI wire?​

Question: How do AgentForge's ChatMessage, ChatRequestParameters, and ChatResponse land on OpenAI's messages / request fields / response fields respectively? How are tool calls mapped bidirectionally? How is streaming aggregated?

This article provides the mapping tables and implementation locations item by item.


1.3 Implementation Boundaries​

  • Wire API: POST {baseUrl}/chat/completions (default https://api.openai.com/v1);
  • release_1.x uses Chat Completions as the first version of the unified protocol, and the Responses API adopts a "coexisting Adapter" strategy (see Chapter 6).

1.4 Builder Parameters​

Builder parameterDefault valueDescription
baseUrlhttps://api.openai.com/v1Also supports OpenAI-compatible services
apiKeynullSends Authorization: Bearer ... when non-empty
modelNamenullMust have a value before the request
temperaturenullOnly sent when non-empty
maxTokensnullMaps to max_tokens
topPnullMaps to top_p
stopSequencesnullMaps to stop
customParameteremptyPasses through Provider-specific top-level fields
customHeaderemptyAdds custom HTTP Headers
httpTransportJdkHttpTransportHTTP SPI
connectTimeoutMillis10000Connection timeout
readTimeoutMillis60000Read timeout

Key point: OpenAiStreamingChatModel reuses the same set of configuration and forcibly writes stream=true and stream_options.include_usage=true.


2. Core Concepts​

2.1 Unified Input​

ChatRequest {
List<ChatMessage> messages;
ChatRequestParameters parameters;
}

ChatRequestParameters {
String modelName();
Double temperature();
Integer maxTokens();
Double topP();
List<String> stopSequences();
Map<String, Object> customParameters();
}

2.2 Unified Output​

ChatResponse {
AiMessage aiMessage; // text + toolExecutionRequests
TokenUsage tokenUsage;
FinishReason finishReason;
Map<String, Object> metadata;
}

Key point: The Provider's responsibility is to land the wire fields losslessly onto these two unified objects.


3. Implementation Approach and Mapping​

3.1 HTTP Headers​

Content-Type: application/json
Accept: application/json
Authorization: Bearer ${apiKey} # when apiKey is non-empty

Afterwards customHeaders is appended; enterprise internal gateways can use customHeader(...) to add tenant, routing, trace, etc.


3.2 Message Mapping​

AgentForgeOpenAI rolecontent / key field
SystemMessagesystemmessage.text()
UserMessage (single text)usermessage.text()
UserMessage (multiple Content)usercontent[], each TextContent → {"type":"text","text":...}
AiMessage (plain text)assistantmessage.text()
AiMessage (with tool calls)assistantcontent (may be null) + tool_calls[]
ToolExecutionResultMessagetooltool_call_id = message.id(), content = message.text()

The wire form of tool calls + result fill-back:

{
"messages": [
{
"role": "assistant",
"content": null,
"tool_calls": [
{ "id": "call_1", "type": "function",
"function": { "name": "getWeather", "arguments": "{\"city\":\"hangzhou\"}" } }
]
},
{ "role": "tool", "tool_call_id": "call_1", "content": "{\"temperature\":22}" }
]
}

Note: CustomMessage currently has no wire mapping implemented; encountering one throws IllegalArgumentException.


3.3 Parameter Mapping​

AgentForge parameterChat Completions fieldCurrent behavior
modelNamemodelRequired; fails locally if empty
temperaturetemperatureOnly sent when non-empty
maxTokensmax_tokensOnly sent when non-empty
topPtop_pOnly sent when non-empty
stopSequencesstopOnly sent when non-empty
toolstools[]{"type":"function","function":{name,description,parameters,strict}}
toolChoicetool_choiceAUTO→"auto", NONE→"none", REQUIRED→"required", SPECIFIC→{"type":"function","function":{"name":X}}
customParametersWritten verbatim at the top levelGeneric fields are written then override same-named custom fields

Construction order (standard fields have final priority):

1. payload.putAll(customParameters)
2. write model / messages
3. write temperature / max_tokens / top_p / stop
4. write tools / tool_choice

Key point: Even if customParameters writes another model, it will ultimately be overridden by modelName.


3.4 Response Mapping​

OpenAI fieldAgentForge field
choices[0].message.contentChatResponse.aiMessage().text()
choices[0].message.tool_calls[]ChatResponse.aiMessage().toolExecutionRequests()
usage.prompt_tokensTokenUsage.inputTokens()
usage.completion_tokensTokenUsage.outputTokens()
usage.total_tokensTokenUsage.totalTokens()
choices[0].finish_reasonChatResponse.finishReason()
id / model / createdmetadata["id"] / ["model"] / ["created"]

tool_calls[] mapping: id → ToolExecutionRequest.id, function.name → name, function.arguments → arguments (preserving the raw JSON string, without reformatting).

Note: When tool_calls is non-empty and content is empty, AiMessage.text() returns null; when both exist, both are preserved.


3.5 finish_reason Mapping​

OpenAIAgentForge FinishReason
stopSTOP
lengthLENGTH
tool_callsTOOL_EXECUTION
function_callTOOL_EXECUTION
content_filterCONTENT_FILTER
Other non-emptyOTHER
nullnull

3.6 Abnormal Response Structures​

The following cases directly throw ModelException (rather than silently returning empty):

choices does not exist / is empty / choices[0].message does not exist

3.7 Streaming Implementation​

Request: Same Endpoint, forcibly stream=true + stream_options.include_usage=true.

SSE parsing: Ignore empty lines, : comments, and non-data: lines; each data: chunk reads choices[0].delta.content, delta.tool_calls[], finish_reason.

  • delta.content → immediately handler.onPartialResponse(...), while aggregating locally with a StringBuilder;
  • delta.tool_calls[] → accumulate bucketed by index (rules in protocol article 4.1), without calling back half-finished pieces;
  • End → handler.onCompleteResponse(response).

Final response:

ChatResponse.builder()
.aiMessage(AiMessage.from(fullText, toolExecutionRequests))
.finishReason(finishReason)
.tokenUsage(tokenUsage)
.metadata(metadata)
.build();

Key point: The final usage chunk (choices=[]) is handled by "read usage first, then determine whether choices is empty"; if the stream is interrupted the final usage may be missing, and ChatResponse.tokenUsage() is not guaranteed to exist.


4. Practical Code​

4.1 Non-Streaming​

OpenAiChatModel model =
OpenAiChatModel.builder()
.baseUrl("https://api.openai.com/v1")
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("your-model")
.temperature(0.2)
.maxTokens(1024)
.build();

ChatRequest request =
ChatRequest.builder()
.message(SystemMessage.from("You are a concise Java assistant."))
.message(UserMessage.from("What is CAS?"))
.build();

ChatResponse response = model.chat(request);
System.out.println(response.aiMessage().text());

Run output (simulated terminal)

$ curl -s https://api.openai.com/v1/chat/completions -H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{"model":"your-model","messages":[{"role":"system","content":"..."},{"role":"user","content":"What is CAS?"}]}'

{"choices":[{"index":0,"message":{"role":"assistant","content":"CAS means Compare-And-Swap."},"finish_reason":"stop"}],
"usage":{"prompt_tokens":20,"completion_tokens":10,"total_tokens":30}}

4.2 Streaming​

OpenAiStreamingChatModel streaming =
OpenAiStreamingChatModel.builder()
.baseUrl("https://api.openai.com/v1")
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("your-model")
.build();

streaming.chat(request, new StreamingChatResponseHandler() {
@Override public void onPartialResponse(String partial) { System.out.print(partial); }
@Override public void onCompleteResponse(ChatResponse response) { System.out.println(); }
@Override public void onError(Throwable error) { error.printStackTrace(); }
});

Run output (simulated terminal)

CAS means Compare-And-Swap.

4.3 OpenAI-compatible Service​

OpenAiChatModel model =
OpenAiChatModel.builder()
.baseUrl("https://example.com/v1")
.apiKey("...")
.modelName("provider-model")
.build();

5. Compatibility, Errors, and Boundaries​

5.1 HTTP Errors​

Non-2xx → new ModelException("OpenAI request failed with HTTP " + statusCode, statusCode, responseBody); network-layer IOException → ModelException("OpenAI request failed", cause). For streaming, a non-2xx accumulates the body and returns it via handler.onError(...).


5.2 Protocol Gaps (Not Mapped in release_1.x)​

developer role, image / audio multimodal content, streaming onPartialToolCall, refusal, logprobs, structured outputs, audio, provider reasoning (the AiMessage.thinking() field slot is reserved but not wired), multiple choices.

customParameters can temporarily pass through request fields; but if the returned structure needs to be understood by the framework, the Core / Provider types must still be formally extended.


6. Summary and Evolution​

Already delivered: request mapping of the tool role's tool_call_id, tools/tool_choice (including SPECIFIC), non-streaming parsing of tool_calls and streaming aggregation by index, UserMessage multiple Content → content[].

OpenAI officially recommends that new applications prioritize the Responses API, so a coexisting Adapter is adopted going forward: OpenAiChatModel → /chat/completions, OpenAiStreamingChatModel → /chat/completions + SSE, with OpenAiResponsesModel → /responses to be added in the future. The upper layer still depends only on the Core interfaces, and protocol migration does not intrude into the Agent Runtime.


References​

[1]. OpenAI Chat Completions API (official reference)

[2]. OpenAI Text Generation Guide

[3]. Related internal documents: A Quick Understanding of the OpenAI Low-Level Protocol, [ChatModel Core Protocol Layer Design](/blog/01、model模型协议层/chatmodel/统一ChatModel协议层01、ChatModel 核心协议层设计:统一 API 契约封装与多协议扩展实现)


Compiler: changlu Created: 2026.10.5 Updated: 2026.10.5